package com.android.monee.domain;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.Months;

import com.android.monee.data.access.DaoFactory;
import com.android.monee.data.model.Budget;
import com.android.monee.data.model.Mensuality;
//import com.android.monee.data.model.Mensuality.MensualityState;
import com.android.monee.domain.MensGrp.MensGrpType;

public class MensGrpOptimizer {

	private List<MensGrp> mensGroups;
	private List<MensGrp> optmzdMensGrps;
	
	private List<BudgetService> budgets;

	public MensGrpOptimizer(){
		this.budgets = BudgetService.newCollection(DaoFactory.getBudgetDao().findAll());
	}

	public MensGrpOptimizer(List<BudgetService> budgets) {
		this.budgets = budgets;
	}

	/*
	private void init(){
		this.mensGroups = MensGrp.calcMensGrps(MensGrpType.UNDONE);
		this.optmzdMensGrps = new ArrayList<MensGrp>();
		
		for(MensGrp mensGroup : this.mensGroups){		
			this.optmzdMensGrps.add((MensGrp) mensGroup.clone());
		}
	}
	*/
	
	private void calcMensGroups(){
		mensGroups = MensGrp.calcMensGroups(budgets);
	}
	
//	private List<DateTime> calcAbsoluteMensualitiesDifference(Collection<BudgetService> budgets){
//		DateTime min, max;
//		min = max = new DateTime();
//		DateTimeComparator comp = DateTimeComparator.getDateOnlyInstance();
//		
//		for(BudgetService budget : budgets){
//			if (budget.hasMensualities()) {
//				DateTime firstDate = new DateTime(budget.getFirstMensuality()
//						.getDate());
//				if (comp.compare(firstDate, min) < 0)
//					min = firstDate;
//
//				DateTime lastDate = new DateTime(budget.getLastMensuality()
//						.getDate());
//				if (comp.compare(lastDate, max) > 0)
//					max = lastDate;
//			}
//		}
//		
//		//Même si les mensualités de tous les budgtes ne forment pas une suite continue, 
//		//on retourne une liste des DateTime pour tous les mois entre min et max inclus;
//		List<DateTime> mensDiffList = new ArrayList<DateTime>();
//		DateTime curr = min;
//		while(Months.monthsBetween(curr, max).getMonths() >= 0){
//			mensDiffList.add(curr);
//			curr = curr.monthOfYear().addToCopy(1);
//		}
//		return mensDiffList;
//	}
	
	//TODO : supprimer optimize et  optimiser optimize2
	public void optimize2(){
		calcMensGroups();
		optmzdMensGrps = mensGroups;
		
//		List<Mensuality> testM = MensGrp.toMensList(optmzdMensGrps);
//		for(Mensuality mens : testM){
//			System.out.println(mens.getBudgetId()+" "+mens.isDone()+" "+mens.getDate().toString());
//		}
		
		//debug : Permet de savoir les delta appliqués en optimisation sur les Mensualités.
		Map<Mensuality, Float> mensOptmz = new HashMap<Mensuality, Float>();

		//au cas où
		int maxIterations = 1000;
		
		float previousMaxAmountDistribuable=0;
		boolean probablyFinished = false;
		Mensuality terminateMens = null;
		
		while(true && maxIterations>0){
			boolean newIteration = false;
			maxIterations--;
			
			//déterminer tous les mensGroup les plus elevés
			List<MensGrp> highestMG = getHighestMensGroups(this.optmzdMensGrps);
			
			//pour chacun des mg les plus elevés
			for(MensGrp hmg : highestMG){
				//System.out.println("Try to reduce MG "+sdf.format(hmg.getDate())+" : "+hmg.getAmount()+" - nbHighestMG : "+highestMG.size());
				
				if(newIteration)
					break;

				//déterminer tous les mensGroup les moins elevés
				List<List<MensGrp>> lowestMGSet = getLowestMensGroups(this.optmzdMensGrps);
				
				// pour chaque liste de mg, du plus faible au plus elevé
				Iterator<List<MensGrp>> lowestMGIt = lowestMGSet.iterator();
				while(lowestMGIt.hasNext()){
					if(newIteration)
						break;
					
					List<MensGrp> lowestMG = lowestMGIt.next();
					
					// pour chaque mensualité du mg le plus elevé
					for (Mensuality hMens : hmg) {
						//System.out.println("HMG : "+hMens.getBudgetId()+" "+hMens.isDone()+" "+hMens.getDate().toString());
						
						//on vérifie que la mensualité (hMens) est > 0
						//et qu'elle appartient à un budget qui possède au moins une mensualité 
						//dans la liste courante des plus faibles mg (lowestMG)
						if (mensualityIsDistribuable(hMens, lowestMG)) {
							//System.out.println("\ttry to distribute amount of "+hMens.getAmount());
							
							//on calcule les mensualités cibles, elle appartiennent 
							//au même budget que la mensualité (hmg) du mg le plus elevé
							//et font partie de la liste courante des plus faibles mg (lowestMG)
							
							//TODO : remettre la propriété "getBudget()" dans Mensuality une fois que Mensuality sera une classe métier et non plsu de persistence
							//BudgetService budget = new BudgetService(hMens.getBudget());
							List<Mensuality> targetMensualities = getMensualitiesOfBudget(hMens.getBudgetId(), lowestMG);
							if(lowestMGIt.hasNext() && !targetMensualities.isEmpty()){			
								List<MensGrp> nextLowestMG = lowestMGIt.next();
								
								//on calcule la différence entre la liste suivante et la courante plus faible de mg
								float maxAmountDistribuable = nextLowestMG.get(0).getTotalAmount()-lowestMG.get(0).getTotalAmount();
								//System.out.println("\tdetermining max amount distribuable "+maxAmountDistribuable+" ("+nextLowestMG.get(0).getAmount()+"-"+lowestMG.get(0).getAmount()+")");
								
								if(maxAmountDistribuable == previousMaxAmountDistribuable && nextLowestMG.get(0).getTotalAmount() == hmg.getTotalAmount()){
									//System.out.println("FINISH ???????????????????????????????????????????????????????????????");
									if(probablyFinished && hMens.getDate().compareTo(terminateMens.getDate()) == 0){
										//System.out.println("REALLY FINISHED.");
										break;
									}
									if(!probablyFinished)
										terminateMens = hMens;
									probablyFinished = true;
								}
								else{
									previousMaxAmountDistribuable = maxAmountDistribuable;
									probablyFinished = false;
									terminateMens = null;
								}
								
								//si on peut tous répartir sur les mensualités 'cibles'
								float amountToDistribute = Math.min(hMens.getAmount(), maxAmountDistribuable*targetMensualities.size());
								distributeAmountOnMensualities(amountToDistribute, targetMensualities, mensOptmz);
								hMens.setAmount(hMens.getAmount()-amountToDistribute);
								
								mensOptmz.put(hMens, -amountToDistribute);
																
								//printTrace(mensOptmz);
								mensOptmz = new HashMap<Mensuality, Float>();
								
								
								//System.out.println("Success to reduce MG "+sdf.format(hmg.getDate())+" : "+hmg.getAmount());
								//Les montant des mensualités ont changé, on recommence
								newIteration = true;
								break;
							}
						}
					}
				}
				
				break;
			}
			
			if(!newIteration)
				break;
		}
		
		Collections.sort(this.optmzdMensGrps);
	}
	
	//TODO : remettre cette méthode
//	private List<Mensuality> getMensualitiesOfBudget(Budget budget, List<MensGrp> mgList) {
//		List<Mensuality> res = new ArrayList<Mensuality>();
//		
//		for(MensGrp mg : mgList){
//			for(Mensuality mens : mg){
//				if(mens.getBudget().equals(budget) && !mens.isDone())
//					res.add(mens);
//			}
//		}
//		
//		return res;
//	}

	public List<MensGrp> optimize(){
		optimize2();
		
		return MensGrp.calcMensGroups(budgets);
	}

	/*
	public List<MensGrp> optimize(){
		
		init();
		
		//debug : Permet de savoir les delta appliqués en optimisation sur les Mensualités.
		Map<Mensuality, Float> mensOptmz = new HashMap<Mensuality, Float>();

		//au cas où
		int maxIterations = 1000;
		
		float previousMaxAmountDistribuable=0;
		boolean probablyFinished = false;
		Mensuality terminateMens = null;
		
		while(true && maxIterations>0){
			boolean newIteration = false;
			maxIterations--;
			
			//déterminer tous les mensGroup les plus elevés
			List<MensGrp> highestMG = getHighestMensGroups(this.optmzdMensGrps);
			
			//pour chacun des mg les plus elevés
			for(MensGrp hmg : highestMG){
				//System.out.println("Try to reduce MG "+sdf.format(hmg.getDate())+" : "+hmg.getAmount()+" - nbHighestMG : "+highestMG.size());
				
				if(newIteration)
					break;

				//déterminer tous les mensGroup les moins elevés
				List<List<MensGrp>> lowestMGSet = getLowestMensGroups(this.optmzdMensGrps);
				
				// pour chaque liste de mg, du plus faible au plus elevé
				Iterator<List<MensGrp>> lowestMGIt = lowestMGSet.iterator();
				while(lowestMGIt.hasNext()){
					if(newIteration)
						break;
					
					List<MensGrp> lowestMG = lowestMGIt.next();
					
					// pour chaque mensualité du mg le plus elevé
					for (Mensuality hMens : hmg) {
						
						//on vérifie que la mensualité (hMens) est > 0
						//et qu'elle appartient à un budget qui possède au moins une mensualité 
						//dans la liste courante des plus faibles mg (lowestMG)
						if (mensualityIsDistribuable(hMens, lowestMG)) {
							//System.out.println("\ttry to distribute amount of "+hMens.getAmount());
							
							//on calcule les mensualités cibles, elle appartiennent 
							//au même budget que la mensualité (hmg) du mg le plus elevé
							//et font partie de la liste courante des plus faibles mg (lowestMG)
							List<Mensuality> targetMensualities = getMensualitiesOfBudget(hMens.getBudgetId(), lowestMG);
							if(lowestMGIt.hasNext()){			
								List<MensGrp> nextLowestMG = lowestMGIt.next();
								
								//on calcule la différence entre la liste suivante et la courante plus faible de mg
								float maxAmountDistribuable = nextLowestMG.get(0).getTotalAmount()-lowestMG.get(0).getTotalAmount();
								//System.out.println("\tdetermining max amount distribuable "+maxAmountDistribuable+" ("+nextLowestMG.get(0).getAmount()+"-"+lowestMG.get(0).getAmount()+")");
								
								if(maxAmountDistribuable == previousMaxAmountDistribuable && nextLowestMG.get(0).getTotalAmount() == hmg.getTotalAmount()){
									//System.out.println("FINISH ???????????????????????????????????????????????????????????????");
									if(probablyFinished && hMens.getDate().compareTo(terminateMens.getDate()) == 0){
										//System.out.println("REALLY FINISHED.");
										break;
									}
									if(!probablyFinished)
										terminateMens = hMens;
									probablyFinished = true;
								}
								else{
									previousMaxAmountDistribuable = maxAmountDistribuable;
									probablyFinished = false;
									terminateMens = null;
								}
								
								//si on peut tous répartir sur les mensualités 'cibles'
								float amountToDistribute = Math.min(hMens.getAmount(), maxAmountDistribuable*targetMensualities.size());
								distributeAmountOnMensualities(amountToDistribute, targetMensualities, mensOptmz);
								hMens.setAmount(hMens.getAmount()-amountToDistribute);
								
								mensOptmz.put(hMens, -amountToDistribute);
																
								//printTrace(mensOptmz);
								mensOptmz = new HashMap<Mensuality, Float>();
								
								
								//System.out.println("Success to reduce MG "+sdf.format(hmg.getDate())+" : "+hmg.getAmount());
								//Les montant des mensualités ont changé, on recommence
								newIteration = true;
								break;
							}
						}
					}
				}
				
				break;
			}
			
			if(!newIteration)
				break;
		}
		
		Collections.sort(this.optmzdMensGrps);
		return this.optmzdMensGrps;
	}
	*/
	
	public Map<MensGrp, Float> getOptmzStats(){
		Map<MensGrp, Float> stats = new HashMap<MensGrp, Float>();
		
		Iterator<MensGrp> srcIt = this.mensGroups.iterator();
		Iterator<MensGrp> optmzIt = this.optmzdMensGrps.iterator();
		
		while(optmzIt.hasNext() && srcIt.hasNext()){
			MensGrp src = srcIt.next();
			MensGrp optz = optmzIt.next();
			
			stats.put(optz, optz.getTotalAmount()-src.getTotalAmount());
		}
		
		return stats;
	}
	
	//TODO : à vérifier mais je crois que cela ne marche pas bien
	public static boolean checkCoherence(List<MensGrp> mensGroups){
		List<Mensuality> allMensualities = new ArrayList<Mensuality>();
		for(MensGrp mg : mensGroups){
			allMensualities.addAll(mg);
		}
		
		Map<Integer, List<Mensuality>> map = new HashMap<Integer, List<Mensuality>>();
		for(Mensuality mens : allMensualities){
			
			if(!map.containsKey(mens.getBudgetId())){
				map.put(mens.getBudgetId(), new ArrayList<Mensuality>());
			}
			map.get(mens.getBudgetId()).add(mens);
		}
		
		for(Integer budgetId : map.keySet()){
			BudgetService budget = BudgetService.findById(budgetId);
			
			List<Mensuality> budgetMens = map.get(budget.getId());

			Collections.sort(budgetMens);
			Mensuality lastMens = budgetMens.get(budgetMens.size()-1);
			
			int lastMensMonth;
			int lastMensYear;
			int termMonth;
			int termYear;
			
			Calendar cal = Calendar.getInstance();
			cal.setTime(lastMens.getDate());
			lastMensMonth = cal.get(Calendar.MONTH);
			lastMensYear = cal.get(Calendar.YEAR);
			
			cal.setTime(budget.getTerm());
			termMonth = cal.get(Calendar.MONTH);
			termYear = cal.get(Calendar.YEAR);
			
			if(lastMensMonth != termMonth || lastMensYear != termYear){
				System.err.println("Error for budget "+budget.getName()+" :");
				System.err.println("\tlastMensMonth "+lastMensMonth+" != termMonth "+termMonth+" || lastMensYear "+lastMensYear+" != termYear "+termYear);
				return false;
			}
			
			float sumMens = 0;
			for(Mensuality mens : budgetMens){
				sumMens += mens.getAmount();
			}
			sumMens += budget.getInitialAmount();
			
			if(sumMens != budget.getTotalAmount()){
				System.err.println("Error for budget "+budget.getName()+" :");
				System.err.println("\tsumMens "+sumMens+" != remainingAmount "+budget.getTotalAmount());
				return false;
			}
		}
	
		return true;
	}
	
	private void printTrace(Map<Mensuality, Float> mensOptmz){
		for(Mensuality mens : mensOptmz.keySet()){
			BudgetService budget = BudgetService.findById(mens.getBudgetId());
			SimpleDateFormat sdf = new SimpleDateFormat("MMMM/yy");
			Float delta = mensOptmz.get(mens);
			
			System.out.println("\t"+sdf.format(mens.getDate()) + " "
					+ budget.getName() + " - "
					+ mens.getNumber() + "/" + budget.getTotalNbMens() + " : "
					+ (delta>0?"+":"")+delta + " -> " + mens.getAmount());
		}
	}
	
	private static void distributeAmountOnMensualities (float amount, List<Mensuality> mensualities, Map<Mensuality, Float> histo){
		//System.out.println("distributeAmountOnMensualities");
		for(Mensuality mens : mensualities){
			//System.out.println(mens.getBudgetId()+" "+mens.isDone()+" "+mens.getDate().toString());
		}
		
		float amountPerMens = new Float(Math.ceil(amount/mensualities.size()));
		
		Collections.sort(mensualities);
		
		for(int i = 0 ; i < mensualities.size() ; i++){
			float mensAmount = amountPerMens;
			if(i == mensualities.size()-1)
				mensAmount = amount;
			
			Mensuality mens = mensualities.get(i);
			mens.setAmount(mens.getAmount()+mensAmount);
			
			amount -= mensAmount;
			
			histo.put(mens, mensAmount);
		}
	}
	
	private static List<Mensuality> getMensualitiesOfBudget(int budgetId, List<MensGrp> mgList){
		List<Mensuality> res = new ArrayList<Mensuality>();
		
		//System.out.println("getMensualitiesOfBudget "+budgetId);
		
		for(MensGrp mg : mgList){
			for(Mensuality mens : mg){
				//System.out.println(mens.getBudgetId()+" "+mens.isDone()+" "+mens.getDate().toString());
				if(mens.getBudgetId()==budgetId && !mens.isDone())
					res.add(mens);
			}
		}
		
		return res;
	}
	
	private static boolean mensualityIsDistribuable(Mensuality mens, List<MensGrp> mglist){
		if(mens.getAmount() <= 0)
			return false;
		
		for(MensGrp mg : mglist){
			for(Mensuality other : mg){
				if(mens.getBudgetId() == other.getBudgetId())
					return true;
			}		
		}
		
		return false;
	}
	
	private List<List<MensGrp>> getLowestMensGroups(List<MensGrp> mensGroups) {
		List<List<MensGrp>> res = new ArrayList<List<MensGrp>>();
		
		Map<Float, List<MensGrp>> map = new HashMap<Float, List<MensGrp>>();

		for (MensGrp mg : mensGroups) {
			List<MensGrp> mgs;
			if (map.containsKey(mg.getTotalAmount())) {
				mgs = map.get(mg.getTotalAmount());
			} else {
				mgs = new ArrayList<MensGrp>();
				map.put(mg.getTotalAmount(), mgs);
			}
			mgs.add(mg);
		}
		
		List<Float> orderedKeys = new ArrayList<Float>(map.keySet());
		Collections.sort(orderedKeys);
		
		for(Float key : orderedKeys){
			res.add(map.get(key));
		}

		return res;
	}

	private List<MensGrp> getHighestMensGroups(List<MensGrp> mensGroups){		
		Collections.sort(mensGroups, new MensGroupAmountDescComparator());

		return getNMensGroupsAmountEquals(0, mensGroups);
	}
	
	private List<MensGrp> getNMensGroupsAmountEquals(int n, List<MensGrp> mensGroups){
		List<MensGrp> res = new ArrayList<MensGrp>();
		
		if(mensGroups.size() > n){
			int i = n;
			float amount = mensGroups.get(i).getTotalAmount();
			
			while(i<mensGroups.size() && mensGroups.get(i).getTotalAmount()==amount){
				res.add(mensGroups.get(i));
				i++;
			}
		}
				
		return res;
	}

	public List<BudgetService> resetOptimizations() {
		// TODO Auto-generated method stub
		return null;
	}
	
	public List<BudgetService> getBudgets() {
		return budgets;
	}

	public void setBudgets(List<BudgetService> budgets) {
		this.budgets = budgets;
	}
}


class MensGroupAmountAscComparator implements Comparator<MensGrp>{

	@Override
	public int compare(MensGrp lhs, MensGrp rhs) {
		return Float.valueOf(lhs.getTotalAmount()).compareTo(Float.valueOf(rhs.getTotalAmount()));
	}
	
}

class MensGroupAmountDescComparator implements Comparator<MensGrp>{

	@Override
	public int compare(MensGrp lhs, MensGrp rhs) {
		return -Float.valueOf(lhs.getTotalAmount()).compareTo(Float.valueOf(rhs.getTotalAmount()));
	}
	
}

class MensAmountDescComparator implements Comparator<Mensuality>{

	@Override
	public int compare(Mensuality lhs, Mensuality rhs) {
		return -Float.valueOf(lhs.getAmount()).compareTo(Float.valueOf(rhs.getAmount()));
	}
	
}

